fix: retain unmodeled CLI wire fields on Assistant/Result messages#1075
Open
Magic-Man-us wants to merge 1 commit into
Open
fix: retain unmodeled CLI wire fields on Assistant/Result messages#1075Magic-Man-us wants to merge 1 commit into
Magic-Man-us wants to merge 1 commit into
Conversation
parse_message dropped stream-json fields it did not model — ttft_ms, terminal_reason, fast_mode_state, request_id, stop_details, and others — because AssistantMessage and ResultMessage, unlike SystemMessage, kept no reference to the raw frame. SDK consumers could not reach those fields without bypassing the SDK and re-parsing the wire. Hybrid fix: - Model the stable scalar metrics as typed attributes: ResultMessage gains ttft_ms / terminal_reason / fast_mode_state; AssistantMessage gains request_id. - Add a raw-frame escape hatch (`data`) to both message types, mirroring the existing SystemMessage.data, so unmodeled and future CLI fields stay reachable without further code changes. Fixes anthropics#1026
There was a problem hiding this comment.
Pull request overview
This PR fixes loss of newer/unmodeled Claude Code CLI stream-json fields by (1) adding typed accessors for stable scalar metrics and (2) retaining the raw wire frame on AssistantMessage and ResultMessage as a forward-compatible escape hatch (aligning with the existing SystemMessage.data pattern).
Changes:
- Add
AssistantMessage.request_idandAssistantMessage.datato preserve the top-level request ID and the full raw frame. - Add
ResultMessage.ttft_ms,terminal_reason,fast_mode_state, andResultMessage.datato expose stable metrics and preserve the raw frame. - Extend parser tests to assert both typed fields and retention of unmodeled/future fields via
.data.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
tests/test_message_parser.py |
Adds coverage ensuring the parser populates new typed fields and retains the raw frame for forward compatibility. |
src/claude_agent_sdk/types.py |
Extends AssistantMessage/ResultMessage dataclasses with typed scalar fields plus optional .data raw-frame storage. |
src/claude_agent_sdk/_internal/message_parser.py |
Populates the new dataclass fields from incoming CLI frames and retains the raw frame on the parsed messages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1026. The CLI emits several
stream-jsonfields thatparse_messageneither reads nor retains, so they vanish from the typed messages. UnlikeSystemMessage(which keeps the raw frame as.data),AssistantMessageandResultMessagekept no reference to the original frame — so consumers could not reach these fields at all without bypassing the SDK and re-parsing the wire.Dropped fields observed on the wire (CLI 2.1.150):
ttft_ms2806(time-to-first-token)terminal_reason"completed"fast_mode_state"off"request_id"req_…"stop_details,diagnostics,context_managementmessage.*nullcontent[].caller{"type": "direct"}Fix — hybrid
The two suggested fixes in the issue (model the fields, or retain the raw frame) each have a gap: pure modeling drifts on the next new CLI field and forces guessed types onto the three fields only ever observed as
null; a pure escape hatch gives no typed access to the stable metrics people actually chart. This PR does both:ResultMessage.ttft_ms/terminal_reason/fast_mode_state, andAssistantMessage.request_id— for ergonomic, type-checked access.data: dict[str, Any] | Noneto both message types, mirroring the existingSystemMessage.data, so the null-only fields (stop_details,diagnostics,context_management),content[].caller, and any future CLI field stay reachable with no further code changes.All new fields default to
Noneand are appended last, so no existing construction or positional usage changes.Test plan
ruff check src/ tests/— cleanruff format --check src/ tests/— cleanmypy src/— cleanpytest tests/— 987 passed, 5 skipped.datahatch (for an unmodeled/future field); verified failing againstmainand passing with the change.